package de.scs.core;

import java.awt.Component;
import java.awt.Container;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.UIDefaults;
import javax.swing.UIManager;

/**
 * A collection of utility methods for Swing.
 * 
 * @author Darryl Burke
 */
public final class SwingUtils
{

  private SwingUtils()
  {
    throw new Error("SwingUtils is just a container for static methods");
  }

  /**
   * Convenience method for searching below <code>container</code> in the component hierarchy and return nested
   * components that are instances of class <code>clazz</code> it finds. Returns an empty list if no such components
   * exist in the container.
   * <P>
   * Invoking this method with a class parameter of JComponent.class will return all nested components.
   * <P>
   * This method invokes getDescendantsOfType(clazz, container, true)
   * 
   * @param clazz the class of components whose instances are to be found.
   * @param container the container at which to begin the search
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfType(Class<T> clazz, Container container)
  {
    return getDescendantsOfType(clazz, container, true);
  }

  /**
   * Convenience method for searching below <code>container</code> in the component hierarchy and return nested
   * components that are instances of class <code>clazz</code> it finds. Returns an empty list if no such components
   * exist in the container.
   * <P>
   * Invoking this method with a class parameter of JComponent.class will return all nested components.
   * 
   * @param clazz the class of components whose instances are to be found.
   * @param container the container at which to begin the search
   * @param nested true to list components nested within another listed component, false otherwise
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfType(Class<T> clazz, Container container, boolean nested)
  {
    List<T> tList = new ArrayList<T>();
    for (Component component : container.getComponents())
    {
      if (clazz.isAssignableFrom(component.getClass()))
      {
        tList.add(clazz.cast(component));
      }
      if (nested || !clazz.isAssignableFrom(component.getClass()))
      {
        tList.addAll(SwingUtils.<T> getDescendantsOfType(clazz, (Container) component, nested));
      }
    }
    return tList;
  }

  /**
   * Convenience method that searches below <code>container</code> in the component hierarchy and returns the first
   * found component that is an instance of class <code>clazz</code> having the bound property value. Returns
   * {@code null} if such component cannot be found.
   * <P>
   * This method invokes getDescendantOfType(clazz, container, property, value, true)
   * 
   * @param clazz the class of component whose instance is to be found.
   * @param container the container at which to begin the search
   * @param property the className of the bound property, exactly as expressed in the accessor e.g. "Text" for
   * getText(), "Value" for getValue().
   * @param value the value of the bound property
   * @return the component, or null if no such component exists in the container
   * @throws java.lang.IllegalArgumentException if the bound property does not exist for the class or cannot be accessed
   */
  public static <T extends JComponent> T getDescendantOfType(Class<T> clazz, Container container, String property,
    Object value) throws IllegalArgumentException
  {
    return getDescendantOfType(clazz, container, property, value, true);
  }

  /**
   * Convenience method that searches below <code>container</code> in the component hierarchy and returns the first
   * found component that is an instance of class <code>clazz</code> and has the bound property value. Returns
   * {@code null} if such component cannot be found.
   * 
   * @param clazz the class of component whose instance to be found.
   * @param container the container at which to begin the search
   * @param property the className of the bound property, exactly as expressed in the accessor e.g. "Text" for
   * getText(), "Value" for getValue().
   * @param value the value of the bound property
   * @param nested true to list components nested within another component which is also an instance of
   * <code>clazz</code>, false otherwise
   * @return the component, or null if no such component exists in the container
   * @throws java.lang.IllegalArgumentException if the bound property does not exist for the class or cannot be accessed
   */
  public static <T extends JComponent> T getDescendantOfType(Class<T> clazz, Container container, String property,
    Object value, boolean nested) throws IllegalArgumentException
  {
    List<T> list = getDescendantsOfType(clazz, container, nested);
    return getComponentFromList(clazz, list, property, value);
  }

  /**
   * Convenience method for searching below <code>container</code> in the component hierarchy and return nested
   * components of class <code>clazz</code> it finds. Returns an empty list if no such components exist in the
   * container.
   * <P>
   * This method invokes getDescendantsOfClass(clazz, container, true)
   * 
   * @param clazz the class of components to be found.
   * @param container the container at which to begin the search
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfClass(Class<T> clazz, Container container)
  {
    return getDescendantsOfClass(clazz, container, true);
  }

  /**
   * Convenience method for searching below <code>container</code> in the component hierarchy and return nested
   * components of class <code>clazz</code> it finds. Returns an empty list if no such components exist in the
   * container.
   * 
   * @param clazz the class of components to be found.
   * @param container the container at which to begin the search
   * @param nested true to list components nested within another listed component, false otherwise
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfClass(Class<T> clazz, Container container, boolean nested)
  {
    List<T> tList = new ArrayList<T>();
    for (Component component : container.getComponents())
    {
      if (clazz.equals(component.getClass()))
      {
        tList.add(clazz.cast(component));
      }
      if (nested || !clazz.equals(component.getClass()))
      {
        tList.addAll(SwingUtils.<T> getDescendantsOfClass(clazz, (Container) component, nested));
      }
    }
    return tList;
  }

  /**
   * Convenience method that searches below <code>container</code> in the component hierarchy in a depth first manner
   * and returns the first found component of class <code>clazz</code> having the bound property value.
   * <P>
   * Returns {@code null} if such component cannot be found.
   * <P>
   * This method invokes getDescendantOfClass(clazz, container, property, value, true)
   * 
   * @param clazz the class of component to be found.
   * @param container the container at which to begin the search
   * @param property the className of the bound property, exactly as expressed in the accessor e.g. "Text" for
   * getText(), "Value" for getValue(). This parameter is case sensitive.
   * @param value the value of the bound property
   * @return the component, or null if no such component exists in the container's hierarchy.
   * @throws java.lang.IllegalArgumentException if the bound property does not exist for the class or cannot be accessed
   */
  public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz, Container container, String property,
    Object value) throws IllegalArgumentException
  {
    return getDescendantOfClass(clazz, container, property, value, true);
  }

  /**
   * Convenience method that searches below <code>container</code> in the component hierarchy in a depth first manner
   * and returns the first found component of class <code>clazz</code> having the bound property value.
   * <P>
   * Returns {@code null} if such component cannot be found.
   * 
   * @param clazz the class of component to be found.
   * @param container the container at which to begin the search
   * @param property the className of the bound property, exactly as expressed in the accessor e.g. "Text" for
   * getText(), "Value" for getValue(). This parameter is case sensitive.
   * @param value the value of the bound property
   * @param nested true to include components nested within another listed component, false otherwise
   * @return the component, or null if no such component exists in the container's hierarchy
   * @throws java.lang.IllegalArgumentException if the bound property does not exist for the class or cannot be accessed
   */
  public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz, Container container, String property,
    Object value, boolean nested) throws IllegalArgumentException
  {
    List<T> list = getDescendantsOfClass(clazz, container, nested);
    return getComponentFromList(clazz, list, property, value);
  }

  private static <T extends JComponent> T getComponentFromList(Class<T> clazz, List<T> list, String property,
    Object value) throws IllegalArgumentException
  {
    T retVal = null;
    Method method = null;
    try
    {
      method = clazz.getMethod("get" + property);
    }
    catch (NoSuchMethodException ex)
    {
      try
      {
        method = clazz.getMethod("is" + property);
      }
      catch (NoSuchMethodException ex1)
      {
        throw new IllegalArgumentException("Property " + property + " not found in class " + clazz.getName());
      }
    }
    try
    {
      for (T t : list)
      {
        Object testVal = method.invoke(t);
        if (equals(value, testVal))
        {
          return t;
        }
      }
    }
    catch (InvocationTargetException ex)
    {
      throw new IllegalArgumentException("Error accessing property " + property + " in class " + clazz.getName());
    }
    catch (IllegalAccessException ex)
    {
      throw new IllegalArgumentException("Property " + property + " cannot be accessed in class " + clazz.getName());
    }
    catch (SecurityException ex)
    {
      throw new IllegalArgumentException("Property " + property + " cannot be accessed in class " + clazz.getName());
    }
    return retVal;
  }

  /**
   * Convenience method for determining whether two objects are either equal or both null.
   * 
   * @param obj1 the first reference object to compare.
   * @param obj2 the second reference object to compare.
   * @return true if obj1 and obj2 are equal or if both are null, false otherwise
   */
  public static boolean equals(Object obj1, Object obj2)
  {
    return obj1 == null ? obj2 == null : obj1.equals(obj2);
  }

  /**
   * Convenience method for mapping a container in the hierarchy to its contained components. The keys are the
   * containers, and the values are lists of contained components.
   * <P>
   * Implementation note: The returned value is a HashMap and the values are of type ArrayList. This is subject to
   * change, so callers should code against the interfaces Map and List.
   * 
   * @param container The JComponent to be mapped
   * @param nested true to drill down to nested containers, false otherwise
   * @return the Map of the UI
   */
  public static Map<JComponent, List<JComponent>> getComponentMap(JComponent container, boolean nested)
  {
    HashMap<JComponent, List<JComponent>> retVal = new HashMap<JComponent, List<JComponent>>();
    for (JComponent component : getDescendantsOfType(JComponent.class, container, false))
    {
      if (!retVal.containsKey(container))
      {
        retVal.put(container, new ArrayList<JComponent>());
      }
      retVal.get(container).add(component);
      if (nested)
      {
        retVal.putAll(getComponentMap(component, nested));
      }
    }
    return retVal;
  }

  /**
   * Convenience method for retrieving a subset of the UIDefaults pertaining to a particular class.
   * 
   * @param clazz the class of interest
   * @return the UIDefaults of the class
   */
  public static UIDefaults getUIDefaultsOfClass(Class clazz)
  {
    String name = clazz.getName();
    name = name.substring(name.lastIndexOf(".") + 2);
    return getUIDefaultsOfClass(name);
  }

  /**
   * Convenience method for retrieving a subset of the UIDefaults pertaining to a particular class.
   * 
   * @param className fully qualified name of the class of interest
   * @return the UIDefaults of the class named
   */
  public static UIDefaults getUIDefaultsOfClass(String className)
  {
    UIDefaults retVal = new UIDefaults();
    UIDefaults defaults = UIManager.getLookAndFeelDefaults();
    List<?> listKeys = Collections.list(defaults.keys());
    for (Object key : listKeys)
    {
      if (key instanceof String && ((String) key).startsWith(className))
      {
        String stringKey = (String) key;
        String property = stringKey;
        if (stringKey.contains("."))
        {
          property = stringKey.substring(stringKey.indexOf(".") + 1);
        }
        retVal.put(property, defaults.get(key));
      }
    }
    return retVal;
  }

  /**
   * Convenience method for retrieving the UIDefault for a single property of a particular class.
   * 
   * @param clazz the class of interest
   * @param property the property to query
   * @return the UIDefault property, or null if not found
   */
  public static Object getUIDefaultOfClass(Class clazz, String property)
  {
    Object retVal = null;
    UIDefaults defaults = getUIDefaultsOfClass(clazz);
    List<Object> listKeys = Collections.list(defaults.keys());
    for (Object key : listKeys)
    {
      if (key.equals(property))
      {
        return defaults.get(key);
      }
      if (key.toString().equalsIgnoreCase(property))
      {
        retVal = defaults.get(key);
      }
    }
    return retVal;
  }

  /**
   * Exclude methods that return values that are meaningless to the user
   */
  static Set<String> setExclude = new HashSet<String>();
  static
  {
    setExclude.add("getFocusCycleRootAncestor");
    setExclude.add("getAccessibleContext");
    setExclude.add("getColorModel");
    setExclude.add("getGraphics");
    setExclude.add("getGraphicsConfiguration");
  }

  /**
   * Convenience method for obtaining most non-null human readable properties of a JComponent. Array properties are not
   * included.
   * <P>
   * Implementation note: The returned value is a HashMap. This is subject to change, so callers should code against the
   * interface Map.
   * 
   * @param component the component whose proerties are to be determined
   * @return the class and value of the properties
   */
  public static Map<Object, Object> getProperties(JComponent component)
  {
    Map<Object, Object> retVal = new HashMap<Object, Object>();
    Class<?> clazz = component.getClass();
    Method[] methods = clazz.getMethods();
    Object value = null;
    for (Method method : methods)
    {
      if (method.getName().matches("^(is|get).*") && method.getParameterTypes().length == 0)
      {
        try
        {
          Class returnType = method.getReturnType();
          if (returnType != void.class && !returnType.getName().startsWith("[")
            && !setExclude.contains(method.getName()))
          {
            String key = method.getName();
            value = method.invoke(component);
            if (value != null && !(value instanceof Component))
            {
              retVal.put(key, value);
            }
          }
          // ignore exceptions that arise if the property could not be accessed
        }
        catch (IllegalAccessException ex)
        {
        }
        catch (IllegalArgumentException ex)
        {
        }
        catch (InvocationTargetException ex)
        {
        }
      }
    }
    return retVal;
  }

  /**
   * Convenience method to obtain the Swing class from which this component was directly or indirectly derived.
   * 
   * @param component The component whose Swing superclass is to be determined
   * @return The nearest Swing class in the inheritance tree
   */
  public static <T extends JComponent> Class getJClass(T component)
  {
    Class<?> clazz = component.getClass();
    while (!clazz.getName().matches("javax.swing.J[^.]*$"))
    {
      clazz = clazz.getSuperclass();
    }
    return clazz;
  }
}
